/*
 * Copyright (c) 2014, Hsu Shih-Chia (schsu01@gmail.com)
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qood;

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;

/**
 * QModel is similar to a <a href="http://en.wikipedia.org/wiki/Table_(database)">Table</a> in Database or DataStore,
 * we suggest using it to replace POJOs (Domain Object with getter/setters).
 * <p/>
 * <p>QModel is immutable (read-only): You can't perform UPDATE or DELETE, but SELECT by using {@link #query()}.
 * <p>The following example illustrates a "SELECT... FROM... GROUP BY... ORDER BY..." operation:
 * <pre><code>
 * final QModel healthDetail = ...;//NAME, AGE, GENDER, WEIGHT, HEIGHT
 * final QModel healthByGender = healthDetail.query()//create a new view about to be queried
 *    .select("GENDER", "COUNT(*)", "MEAN(WEIGHT)", "MEAN(HEIGHT)")
 *    .groupBy("GENDER")
 *    .orderBy("COUNT(*)")
 *    .result();//executed as a new QModel
 * </code></pre>
 *
 * @author SCHsu01
 * @version 1.0
 */
public interface QModel extends Serializable, Comparable {
  /**
   * Get name of all columns in this QModel
   *
   * @return the column names
   */
  String[] getColNames();

  /**
   * Get how many columns in this QModel
   *
   * @return column count
   */
  int getColCount();

  /**
   * Get how many rows in this QModel
   *
   * @return row count
   */
  int getRowCount();

  /**
   * Get cell value by specified position
   *
   * @param row rowId, between 0 to ({@link #getRowCount()}-1)
   * @param col colId, between 0 to ({@link #getColCount()}-1)
   * @return cell value
   */
  Object getValue(int row, int col);

  /**
   * Print data to System.out for debugging, prints all if rowCnt &lt; 0.
   *
   * @param rowCnt the rows to be printed out
   * @return a reference to this Object
   */
  QModel debug(int rowCnt);

  /**
   * Create a new view about to be queried, which {@link QView#from(String)} default is this QModel
   *
   * @return the new view
   */
  QView query();

  /**
   * Get a interface to convert data in this QModel
   *
   * @return this QModel's output interface
   */
  QOutput to();
}

/**
 * Should be super class for all QModel implementation in version 1.0
 */
abstract class Model implements QModel {
  final String[] name;
  private transient Output out;

  Model(String... name) {
    this.name = name;
  }

  abstract Col[] getCols();//key feature

  public final String[] getColNames() {
    return name;
  }

  public final int getColCount() {
    return name.length;
  }

  protected Object writeReplace() throws ObjectStreamException {
    return new Q(this);
  }

  public final Model debug(int rowCnt) {
    final int max = getRowCount();
    to().log(System.out, IntRead.nSize(rowCnt < 0 || rowCnt > max ? max : rowCnt), name);
    return this;
  }

  public final View query() {
    final Map<String, Model> source = Collections.singletonMap(null, this);
    return new View(source, new SQL());
  }

  public Output to() {
    return null == out ? out = new Output(this) : out;
  }

  @Override
  public final String toString() {
    return getClass().getSimpleName() + '(' + getColCount() + 'x' + getRowCount() + ')';
  }

  public int compareTo(Object o) {
    if (o instanceof QModel) {
      final QModel q = (QModel) o;
      final int c = getColCount() - q.getColCount();
      return 0 == c ? getRowCount() - q.getRowCount() : c;
    } else return toString().compareTo(String.valueOf(o));
  }
}

abstract class ModelRow extends Model {
  private transient Col[] cols;

  ModelRow(String[] name) {
    super(name);
  }

  final Col[] getCols() {
    if (null == cols) {
      final int n = getColCount();
      cols = new Col[n];
      for (int i = 0; i < n; i++) cols[i] = createCol(i);
    }
    return cols;
  }

  abstract Col createCol(int col);
}

final class ModelArray extends ModelRow {
  private final Class[] types;
  private final Object[][] data;

  ModelArray(String[] name, Class[] types, Object[][] data) {
    super(name);
    this.types = types;
    this.data = data;
  }

  Col createCol(int col) {
    return new C(col, types[col]);
  }

  public int getRowCount() {
    return data.length;
  }

  public Object getValue(int row, int col) {
    return data[row][col];
  }

  private final class C extends Col {
    private final int col;
    private final Class type;

    private C(int col, Class type) {
      this.col = col;
      this.type = type;
    }

    Object getRow(int row) {
      return data[row][col];
    }

    Class getType() {
      return type;
    }

    int expectSize() {
      return getRowCount();
    }

    @Override
    int compare(int row1, int row2) {
      return Formula.compare((Comparable) data[row1][col], (Comparable) data[row2][col]);
    }
  }
}

final class ModelMaps extends ModelRow {//slow and waste
  private final Class[] types;
  private final Map[] data;

  ModelMaps(Map[] data, String[] names, Class... types) {
    super(names);
    this.types = types;
    this.data = data;
  }

  Col createCol(int col) {
    return new C(name[col], types[col]);
  }

  public int getRowCount() {
    return data.length;
  }

  public Object getValue(int row, int col) {
    return data[row].get(name[col]);
  }

  private final class C extends Col {
    private final String name;
    private final Class type;

    private C(String name, Class type) {
      this.name = name;
      this.type = type;
    }

    Object getRow(int row) {
      return data[row].get(name);
    }

    Class getType() {
      return type;
    }

    int expectSize() {
      return getRowCount();
    }
  }
}

final class ModelPOJO extends ModelRow {
  private final Object[] values;
  private final Converter[] getter;

  ModelPOJO(String[] name, Object[] values, Converter... getter) {
    super(name);
    this.values = values;
    this.getter = getter;
  }

  Col createCol(int col) {
    return new C(getter[col]);
  }

  public int getRowCount() {
    return values.length;
  }

  @SuppressWarnings({"unchecked"})
  public Object getValue(int row, int col) {
    try {
      return getter[col].to(values[row]);
    } catch (Exception e) {
      return null;
    }
  }

  private final class C extends Col {
    private final Converter map;

    private C(Converter map) {
      this.map = map;
    }

    @SuppressWarnings({"unchecked"})
    Object getRow(int row) {
      return map.to(values[row]);
    }

    Class getType() {
      return map.toType();
    }

    int expectSize() {
      return getRowCount();
    }
  }
}

final class ModelBean<T> extends ModelRow implements CacheList.Loader<Object[]> {
  private final QOutput.Bean<T> convert;
  private final T[] rows;
  private final CacheList<Object[]> cache;

  ModelBean(QOutput.Bean<T> convert, T... rows) {
    super(convert.fields());
    this.convert = convert;
    this.rows = rows;
    this.cache = new CacheList<Object[]>(this);
  }

  Col createCol(int col) {
    return new C(col, convert.types()[col]);
  }

  public int getRowCount() {
    return rows.length;
  }

  public Object getValue(int row, int col) {
    return cache.get(row)[col];
  }

  public Object[] loadForCache(int index) {
    return convert.from(rows[index]);
  }

  private final class C extends Col {
    private final int col;
    private final Class type;

    private C(int col, Class type) {
      this.col = col;
      this.type = type;
    }

    Object getRow(int row) {
      return cache.get(row)[col];
    }

    Class getType() {
      return type;
    }

    int expectSize() {
      return getRowCount();
    }

    @Override
    int compare(int row1, int row2) {
      return Formula.compare((Comparable) cache.get(row1)[col], (Comparable) cache.get(row2)[col]);
    }
  }
}

final class ModelCol extends Model {
  private final int rows;
  private final Col[] cols;

  ModelCol(int rows, Col[] cols, String... name) {
    super(name);
    this.rows = rows;
    this.cols = cols;
  }

  Col[] getCols() {
    return cols;
  }

  public int getRowCount() {
    return rows;
  }

  public Object getValue(int row, int col) {
    return cols[col].getRow(row);
  }
}

final class ModelTrans extends Model {//for Transpose a Matrix
  private final QModel model;
  private transient Col[] cols;

  static QModel getInstance(QModel model) {
    return model instanceof ModelTrans ? ((ModelTrans) model).model : new ModelTrans(model);
  }

  private ModelTrans(QModel model) {
    super(getName("C", 0, model.getRowCount() + 1));
    this.model = model;
  }

  static String[] getName(String prefix, int start, int count) {
    final String[] result = new String[count];
    for (int i = 0; i < count; i++) result[i] = prefix + start++;
    return result;
  }

  Col[] getCols() {
    if (null == cols) {
      final int n = getColCount();
      cols = new Col[n];
      cols[0] = Col.getValues(String.class, model.getColNames());
      for (int i = 1; i < n; i++) cols[i] = new C(i - 1);
    }
    return cols;
  }

  public int getRowCount() {
    return model.getColCount();
  }

  public Object getValue(int row, int col) {
    return getCols()[col].getRow(row);
  }

  private final class C extends Col {
    private final int i;

    C(int i) {
      this.i = i;
    }

    Object getRow(int row) {
      return model.getValue(i, row);
    }

    Class getType() {//unknown
      return Object.class;
    }

    int expectSize() {
      return model.getColCount();
    }
  }
}

abstract class Col {
  private static final Col ROWID = new ColId(null);

  static Col getId(IntRead id) {
    return null == id ? ROWID : new ColId(id);
  }

  static Col[] toArray(Collection<Col> collection) {
    return collection.toArray(new Col[collection.size()]);
  }

  static <T> Col getConst(Class<? extends T> typeNonNull, T valueNullable) {
    return new ColConst<T>(valueNullable, typeNonNull);
  }

  static Class[] typeOf(Col... cols) {
    final int n = cols.length;
    final Class[] result = new Class[n];
    for (int i = 0; i < n; i++) result[i] = cols[i].getType();
    return result;
  }

  static Class[] typeOf(Object... values) {
    final int n = values.length;
    final Class[] result = new Class[n];
    for (int i = 0; i < n; i++) {
      final Object v = values[i];
      result[i] = null == v ? Comparable.class : v.getClass();
    }
    return result;
  }

  @SuppressWarnings({"unchecked", "SuspiciousSystemArraycopy"})
  static Col getValues(/*notNull*/ Class type, Object[] values) {
    if (Comparable.class.isAssignableFrom(values.getClass().getComponentType()))
      return new ColComp((Comparable[]) values, type);
    try {
      final int count = values.length;
      final Comparable[] comps = new Comparable[count];
      System.arraycopy(values, 0, comps, 0, count);
      return new ColComp(comps, type);
    } catch (ArrayStoreException e) {//cannot copy to Comparable
      return new ColHeap(values, type);
    }
  }

  static Col[] getPartial(IntRead rows, Col... col) {
    final Map<IntRead, IntRead> map = new IdentityHashMap<IntRead, IntRead>();
    final int n = col.length;
    final Col[] result = new Col[n];
    for (int i = 0; i < n; i++) result[i] = col[i].toPartial(rows, map);
    return result;
  }

  static IntRead mapRows(IntRead from, IntRead to, Map<IntRead, IntRead> map) {
    IntRead rows = map.get(from);
    if (null == rows) map.put(from, rows = IntRead.mapping(from, to));
    return rows;
  }

  abstract Object getRow(int row);

  abstract Class getType();

  abstract int expectSize();

  @SuppressWarnings("unchecked")
  int compare(int row1, int row2) {
    return Formula.compare(getRow(row1), getRow(row2));
  }

  boolean isConstant() {
    return false;
  }

  Col toPartial(IntRead rows, Map<IntRead, IntRead> map) {
    return rows.isSimple() ? this : new ColPart(this, rows);
  }

  /**
   * @return isAggregate is for executing query, not for schema
   */
  boolean isAggregate() {
    return false;
  }
}

class ColHeap<T> extends Col {
  final T[] rows;
  private final Class<T> type;

  ColHeap(T[] rows, Class<T> type) {
    this.rows = rows;
    this.type = type;
  }

  final T getRow(int row) {
    return rows[row];
  }

  final Class<T> getType() {
    return type;
  }

  final int expectSize() {
    return rows.length;
  }
}

final class ColComp<T extends Comparable> extends ColHeap<T> {
  ColComp(T[] rows, Class<T> type) {
    super(rows, type);
  }

  @Override
  int compare(int row1, int row2) {//faster
    return Formula.compare(rows[row1], rows[row2]);
  }
}

final class ColConst<T> extends Col {
  private final Class<? extends T> type;
  private final T value;

  ColConst(T value, Class<? extends T> type) {
    this.value = value;
    this.type = type;
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + "=" + value;
  }

  T getRow(int row) {
    return value;
  }

  Class getType() {
    return type;
  }

  int expectSize() {
    return -1;
  }

  @Override
  boolean isConstant() {
    return true;
  }

  @Override
  int compare(int row1, int row2) {
    return 0;
  }
}

final class ColId extends Col {
  private final IntRead id;

  ColId(IntRead id) {
    this.id = id;
  }

  Object getRow(int row) {
    return null == id ? row : id.get(row);
  }

  Class getType() {
    return Integer.class;
  }

  int expectSize() {
    return null == id ? -1 : id.size();
  }

  @Override
  int compare(int row1, int row2) {
    return null == id ? row1 - row2 : id.get(row1) - id.get(row2);
  }

  @Override
  Col toPartial(IntRead rows, Map<IntRead, IntRead> map) {
    return rows.isSimple() ? this : getId(null == id ? rows : mapRows(id, rows, map));
  }
}

/**
 * for solving "isAggregate exposure" problem
 */
final class ColWrap extends Col {
  private final Col col;

  ColWrap(Col col) {
    this.col = col;
  }

  Object getRow(int row) {
    return col.getRow(row);
  }

  Class getType() {
    return col.getType();
  }

  int expectSize() {
    return col.expectSize();
  }

  @Override
  int compare(int row1, int row2) {
    return col.compare(row1, row2);
  }
}

final class ColPart extends Col {
  private final Col col;
  private final IntRead rows;

  ColPart(Col col, IntRead rows) {
    this.col = col;
    this.rows = rows;
  }

  Object getRow(int row) {
    final int id = rows.get(row);
    return id >= 0 ? col.getRow(id) : null;
  }

  Class getType() {
    return col.getType();
  }

  int expectSize() {
    return rows.size();
  }

  @Override
  int compare(int row1, int row2) {
    final int id1 = rows.get(row1), id2 = rows.get(row2);
    return id1 >= 0 && id2 >= 0 ? col.compare(id1, id2) : id1 - id2;
  }

  @Override
  boolean isAggregate() {
    return col.isAggregate();
  }

  @Override
  Col toPartial(IntRead rows, Map<IntRead, IntRead> map) {
    return rows.isSimple() ? this : col.toPartial(mapRows(this.rows, rows, map), map);
  }
}